﻿(function () {
    class WebAutoError extends Error {
        constructor(message) {
            super(message || "")
        }
    }
    //html中各种标签
    const TAGS = {
        BODY: 'BODY',
        TABLE: 'TABLE',
        INPUT: 'INPUT',
        BUTTON: 'BUTTON',
        SELECT: 'SELECT',
        LABEL: 'LABEL',
        TEXTAREA: 'TEXTAREA',
        IFRAME: 'IFRAME',
        FRAME: 'FRAME',
        A: 'A',
        IMG: 'IMG',
        SCRIPT: 'SCRIPT'
    }

    const KEY_MODIFIERS = {
        NONE: 0,
        ALT: 1,
        CTRL: 2,
        SHIFT: 4
    }

    const CLICK_TYPE = {
        LCLICK: 0,
        RCLICK: 1,
        LDCLICK: 2,
        RDCLICK: 3,
        MIDDLE_CLICK: 4,
        HOVER: 5
    }

    const MATCH_MODE = {
        FUZZY: 0,
        EXACT: 1,
        REGEX: 2
    }

    class SubFrame {
        constructor(frame, originalParam) {
            this.param = originalParam
            this.frameIndex = DomUtil.GetFrameIndex(frame.contentWindow)
        }
    }

    class Rect {
        constructor(x, y, width, height) {
            this.x = x
            this.y = y
            this.width = width
            this.height = height
        }

        contains(point) {
            return point.x >= this.x && point.x <= (this.width + this.x) && point.y >= this.y && point.y <= (this.height + this.y)
        }

        center() {
            return {
                x: Math.round(this.x + this.width / 2),
                y: Math.round(this.y + this.height / 2)
            }
        }

        offset(x, y) {
            this.x += x
            this.y += y
        }

        scale(ratio) {
            return new Rect(Math.round(this.x * ratio), Math.round(this.y * ratio),
                Math.round(this.width * ratio), Math.round(this.height * ratio))
        }

        ScaleInv(ratio) {
            return new Rect(Math.round(this.x / ratio), Math.round(this.y / ratio),
                Math.round(this.width / ratio), Math.round(this.height / ratio))
        }

        intersect(rect) {
            const x1 = Math.max(this.x, rect.x)
            const x2 = Math.min(this.x + this.width, rect.x + rect.width)
            const y1 = Math.max(this.y, rect.y)
            const y2 = Math.min(this.y + this.height, rect.y + rect.height)
            if (x2 >= x1 && y2 >= y1) {
                return new Rect(x1, y1, x2 - x1, y2 - y1)
            } else {
                return null
            }
        }

        static fromDOMRect(domRect) {
            return new Rect(Math.round(domRect.x), Math.round(domRect.y),
                Math.round(domRect.width), Math.round(domRect.height))
        }
    }

    class Element {
        constructor(frameLevel, xPath) {
            this.frameLevel = frameLevel;
            this.xPath = xPath
        }

        static scrollIntoViewIfNeeded = (ele) => { // 考虑占据大部分情况的竖向布局网页
            if (!ele) { 
                return;
            }
            let needScroll = false;

            //滚动条高度+视窗高度 = 可见区域底部高度
            let scrollParent = DomUtil.GetScrollableParent(ele, "vertical");
            var visibleBottom = scrollParent.scrollTop + scrollParent.clientHeight;
            //可见区域顶部高度
            var visibleTop = scrollParent.scrollTop;
            var centerY = ele.offsetTop+(ele.offsetHeight/2);
            if (centerY < visibleTop || centerY > visibleBottom) {
                needScroll = true;
            }
            if (needScroll) { 
                DomUtil.ScrollIntoViewIfNeeded(ele);
            }
        }
    }


    //dom操作工具类
    class DomUtil {
        static FocusEle(element){
            element.focus()
        }
    
        static GetClickType(clickType, buttonType){
            let howToClick = CLICK_TYPE.LCLICK;
            if(clickType === 0){
                //单击
                if(buttonType === 0){
                    //左键
                    howToClick = CLICK_TYPE.LCLICK;
                }else{
                    //右键
                    howToClick = CLICK_TYPE.RCLICK;
                }
            }else{
                //双击
                if(buttonType === 0){
                    howToClick = CLICK_TYPE.LDCLICK;
                }else{
                    howToClick = clickType.RDCLICK;
                }
            }
            return howToClick;
        }
        static matchElementType(ele, ...tags) {
            if (!!ele && ele.nodeType === Node.ELEMENT_NODE) {
                const curTag = DomUtil.GetTagName(ele).toUpperCase()
                for (const tag of tags) {
                    if (tag === curTag) {
                        return true
                    }
                }
            }
            return false
        }

        static IsFrame(ele) {
            return DomUtil.matchElementType(ele, TAGS.IFRAME, TAGS.FRAME)
        }

        static IsEditable(element) {
            const nodeName = element.nodeName.toLowerCase()
            if (element.nodeType == 1 && (nodeName == "textarea" ||
                    (nodeName == "input" && /^(?:text|email|number|search|tel|url|password)$/i.test(element.type)) 
                        || element.isContentEditable)) {
                return element.disabled !== true && element.readOnly !== true
            } else {
                return false
            }
        }
        
        static GetFrameOffset(frame) {
            let pLeft = parseInt(window.getComputedStyle(frame, null).getPropertyValue('padding-left')) || 0
            let pTop = parseInt(window.getComputedStyle(frame, null).getPropertyValue('padding-top')) || 0
            let bLeft = parseInt(window.getComputedStyle(frame, null).getPropertyValue('border-left-width')) || 0
            let bTop = parseInt(window.getComputedStyle(frame, null).getPropertyValue('border-top-width')) || 0
            return {
                x: pLeft + bLeft,
                y: pTop + bTop
            }
        }

        //获取子frame在父frame中的索引位置，如果frame本身是主frame即frameid是0，则返回-1
        static GetFrameIndex(frame) {
            if (frame.parent === frame || frame.parent === null) {
                return -1
            } else {
                const wnds = frame.parent.frames
                for (let i = 0; i < wnds.length; i++) {
                    if (wnds[i] === frame) {
                        return i
                    }
                }
                return -1
            }
        }

        static ElementFromPoint(args) {
            return document.elementFromPoint(args.x, args.y)
        }

        static GetTagName(ele) {
            //获取节点名称
            // 某些情况下Form标签的tagName是input元素
            if (typeof (ele.tagName) === 'string') {
                return ele.tagName.toLowerCase()
            } else {
                return ele.nodeName.toLowerCase()
            }

        }

        static GetEleByXpath(xPath, parent = null){
            const root = parent || document
            var xPathResult = document.evaluate(xPath, root, null, XPathResult.ANY_TYPE, null)
            var elements = []
            let element = undefined;
            while (element = xPathResult.iterateNext()) {
                elements.push(element)
            }
            return elements
        }

        static GetEleXpath(ele) {
            if (ele.id !== "") {//判断id属性，如果这个元素有id，则显 示//*[@id="xPath"]  形式内容
                return '//*[@id=\"' + ele.id + '\"]';
            }
            //这里需要需要主要字符串转译问题，可参考js 动态生成html时字符串和变量转译（注意引号的作用）
            if (ele == document.body) {//递归到body处，结束递归
                return '/html/' + ele.tagName.toLowerCase();
            }
            var ix = 1,//在nodelist中的位置，且每次点击初始化
            siblings = ele.parentNode.childNodes;//同级的子元素

            for (var i = 0, l = siblings.length; i < l; i++) {
                var sibling = siblings[i];
                //如果这个元素是siblings数组中的元素，则执行递归操作
                if (sibling == ele) {
                    return DomUtil.GetEleXpath(ele.parentNode) + '/' + ele.tagName.toLowerCase() + '[' + (ix) + ']';
                    //如果不符合，判断是否是element元素，并且是否是相同元素，如果是相同的就开始累加
                } else if (sibling.nodeType == 1 && sibling.tagName == ele.tagName) {
                    ix++;
                }
            }
        }

        static RaiseClickEvent = (clickType, element, x, y, keyModifiers) => {
            let types = []
            let button = 0
            if (clickType === CLICK_TYPE.HOVER) {
                types = ["mouseover", "mouseenter", "mousemove"]
                button = 0
            } else if (clickType === CLICK_TYPE.LDCLICK) {
                types = ["mousedown", "mouseup", "click", "mousedown", "mouseup", "click", "dblclick"]
                button = 0
            } else if (clickType === CLICK_TYPE.RCLICK) {
                types = ["mousedown", "mouseup", "contextmenu"]
                button = 2
            } else if (clickType === CLICK_TYPE.MIDDLE_CLICK) {
                types = ["mousedown", "mouseup", "click"]
                button = 1
            } else {
                types = ["mousedown", "mouseup", "click"]
                button = 0
            }
            const ctrlKey = !!(keyModifiers & KEY_MODIFIERS.CTRL)
            const altKey = !!(keyModifiers & KEY_MODIFIERS.ALT)
            const shiftKey = !!(keyModifiers & KEY_MODIFIERS.SHIFT)
            let waitTime = 0 //延时执行，保证event生效  尝试了时间递增1不可以
            for (const type of types) {
                waitTime += 50
                setTimeout(() => {
                    var evt = document.createEvent("MouseEvents")
                    evt.initMouseEvent(type, true, true, window, 1, 0, 0, x, y, ctrlKey, altKey, shiftKey, false, button, null)
                    element.dispatchEvent(evt)
                }, waitTime)
            }
        }

        static RaiseClickOnElement = (element) => {
            const rect = element.getBoundingClientRect()
            DomUtil.RaiseClickEvent(CLICK_TYPE.CLICK, element, rect.x + rect.width / 2, rect.y + rect.height / 2, KEY_MODIFIERS.NONE)
        }

        static ScrollIntoViewIfNeeded = (element) => {
            //https://developer.mozilla.org/zh-CN/docs/Web/API/Element/scrollIntoViewIfNeeded
            //firefox的元素不支持scrollIntoViewIfNeeded方法，此处需要加判断，目前还未找到firefox类似scrollIntoViewIfNeeded的方法
            //此处先用scrollIntoView代替
            // if (window.mozInnerScreenX !== undefined)
            //     element.scrollIntoView()
            // else
            //     element.scrollIntoViewIfNeeded() //默认参数为True，让元素在可视区域中居中对齐
            // window.scrollTo(element.offsetLeft, 0);
            // window.scrollTo(0, element.offsetTop);
            //竖向
            let vp = DomUtil.GetScrollableParent(element, "vertical");
            //横向
            //let hp = DomUtil.GetScrollableParent(element, "horizontal");
            if (vp) { 
                vp.scrollTo(0, element.offsetTop);
            }
            // if (hp) { 
            //     hp.scrollTo(0, element.offsetTop);
            // }
        }

        //获取元素拥有滚动条的父节点直到document
        static GetScrollableParent = (element, direction) => {
            var ret = element;
            if (direction == "vertical") {
                while (ret) {
                    if (ret.scrollHeight <= ret.clientHeight) {
                        ret = ret.parentElement;
                    } else {
                        break;
                    }
                }
            } else if (direction == "horizontal") {
                while (ret) {
                    if (ret.scrollWidth <= ret.clientWidth) {
                        ret = ret.parentElement;
                    } else {
                        break;
                    }
                }
            }

            if (!ret) {
                ret = element;
            }
            return ret;
        }
    }

    window.conDispatcher = new function () {
        const actions = {
            GetFrameIndex: function (param) {
                return DomUtil.GetFrameIndex(window)
            },
            CatchElement: function (param) {
                let element = DomUtil.ElementFromPoint(param);
                if (element) {
                    if (DomUtil.IsFrame(element)) {
                        let offset = DomUtil.GetFrameOffset(element)
                        let bounding = element.getBoundingClientRect()
                        param.x = param.x - bounding.x - offset.x;
                        param.y = param.y - bounding.y - offset.y;
                        return new SubFrame(element, param)
                    } else {
                        let xpath = DomUtil.GetEleXpath(element);
                        return new Element(param.frameLevel, xpath);
                    }
                } else {
                    throw new WebAutoError("该位置不存在元素");
                }
            },
            ClickEle: (args) => {
                let elements = DomUtil.GetEleByXpath(args.ele.xpath);
                if(!elements || elements.length === 0){
                    throw new WebAutoError("没有找到元素，无法进行点击操作");
                }
                let element = elements[0];
                Element.scrollIntoViewIfNeeded(element);
                let rect = element.getBoundingClientRect()
                let howToClick = DomUtil.GetClickType(args.clickType, args.buttonType);
                DomUtil.RaiseClickEvent(howToClick, element, rect.x + rect.width / 2, rect.y + rect.height / 2, KEY_MODIFIERS.NONE) //在元素的中心点击
            },
            
            InputTextToEle: (args) => {
                let elements = DomUtil.GetEleByXpath(args.ele.xpath)
                if(!elements || elements.length === 0){
                    throw new WebAutoError("没有找到元素，无法进行输入操作");
                }
                let element = elements[0];
                let editable = DomUtil.IsEditable(element);
                if (!editable) { 
                    throw new WebAutoError("元素无法编辑，无法进行输入操作");
                }
                Element.scrollIntoViewIfNeeded(element);
                const nodeName = element.nodeName.toLowerCase()
                if (nodeName == 'textarea' || nodeName == 'input') {
                    if (args.append) {
                        element.value = element.value + args.inputText
                    } else {
                        element.value = args.inputText
                    }
                } else {
                    if (args.append) {
                        element.innerText = element.innerText + args.inputText
                    } else {
                        element.innerText = args.inputText
                    }
                }
                DomUtil.FocusEle(element);
            },

            GetFocus:(args)=>{ 
                let elements = DomUtil.GetEleByXpath(args.ele.xpath)
                if(!elements || elements.length === 0){
                    throw new WebAutoError("没有找到元素，无法获取焦点");
                }
                let element = elements[0];
                Element.scrollIntoViewIfNeeded(element);
                DomUtil.FocusEle(element);
            },
            SelectDropDownValue: (args) => {
                // items, mode, append, multiple
                const elements = DomUtil.GetEleByXpath(args.ele.xpath)
                if(!elements || elements.length === 0){
                    throw new WebAutoError("没有找到元素，无法进行设置下拉框操作");
                }
                let element = elements[0];
                if (!DomUtil.matchElementType(element, TAGS.SELECT)) {
                    throw new WebAutoError('此元素不支持设置下拉项操作')
                }
                Element.scrollIntoViewIfNeeded(element);
                function IsMatch(text) {
                    if (args.matchMethod === MATCH_MODE.EXACT) {
                        return args.selectValue === text;
                    } else if (args.matchMethod === MATCH_MODE.FUZZY) {
                        return text && text.indexOf(args.selectValue) > -1
                       
                    } else { // MATCH_MODE.REGEX
                        //return args.items.some((m) => {
                        return (new RegExp(args.selectValue)).test(text)
                        //})
                    }
                }

                //if (args.multiple && !args.append) {
                    for (const option of element.options) {
                        option.selected = false
                    }
                //}

                for (const option of element.options) {
                    if (IsMatch(option.text)) {
                        option.selected = true
                        //if (!args.multiple) {
                            break
                        //}
                    }
                }

                const event = document.createEvent('Events');
                event.initEvent('change', true, false);
                element.dispatchEvent(event);
            },

            SelectDropDownByIndex: (args) => {
                // indexes, append, multiple 
                const elements = DomUtil.GetEleByXpath(args.ele.xpath)
                if(!elements || elements.length === 0){
                    throw new WebAutoError("没有找到元素，无法进行设置下拉框操作");
                }
                let element = elements[0];
                if (!DomUtil.matchElementType(element, TAGS.SELECT)) {
                    throw new WebAutoError('此元素不支持设置下拉项操作')
                }
                Element.scrollIntoViewIfNeeded(element);

                //if (args.multiple && !args.append) {
                    for (const option of element.options) {
                        option.selected = false
                    }
                //}

                // for (const option of element.options) {
                //     if (args.indexes.indexOf(option.index) > -1) {
                //         option.selected = true
                //         if (!args.multiple) {
                //             break
                //         }
                //     }
                // }
                element.options[parseInt(args.selectValue)].selected = true;

                const event = document.createEvent('Events');
                event.initEvent('change', true, false);
                element.dispatchEvent(event);
            },
            GetDropDownOptions: (args) => {
                const elements = DomUtil.GetEleByXpath(args.ele.xpath)
                if(!elements || elements.length === 0){
                    throw new WebAutoError("没有找到元素，无法进行获取下拉项操作");
                }
                let element = elements[0];
                if (!DomUtil.matchElementType(element, TAGS.SELECT)) {
                    throw new WebAutoError('此元素不支持获取下拉项操作')
                }
                Element.scrollIntoViewIfNeeded(element);
                return [...element.options].map((m) => {
                    return {
                        name: m.text,
                        value: m.value,
                        selected: m.selected
                    }
                })
            },
            CheckRadioOrCheckBox: (args) => {
                const elements = DomUtil.GetEleByXpath(args.ele.xpath)
                // label -> input
                if (DomUtil.matchElementType(element, TAGS.LABEL)) {
                    var forId = element.htmlFor;
                    if (forId != null && forId.length !== 0) {
                        var targetElement = element.ownerDocument.getElementById(forID);
                        if (targetElement != null) {
                            element = targetElement;
                        }
                    }
                }
                if (!DomUtil.matchElementType(element, TAGS.INPUT)) {
                    throw new WebAutoError('此元素不支持勾选操作')
                }
                Element.scrollIntoViewIfNeeded(element);
                if (element.type === 'radio') {
                    if (args.mode !== CHECK_MODE.CHECK) {
                        throw new WebAutoError('radio元素不支持取消勾选操作')
                    } else {
                        element.checked || DomUtil.RaiseClickOnElement(element)
                    }
                } else if (element.type === 'checkbox') {
                    if (args.mode === CHECK_MODE.CHECK) {
                        !element.checked && DomUtil.RaiseClickOnElement(element)
                    } else if (args.mode === CHECK_MODE.UNCHECK) {
                        element.checked && DomUtil.RaiseClickOnElement(element)
                    } else {
                        DomUtil.RaiseClickOnElement(element) // CHECK_MODE.TOGGLE
                    }
                } else {
                    throw new WebAutoError('此元素不支持勾选操作')
                }
            },
            GetIsCheck: (args) => {
                const elements = DomUtil.GetEleByXpath(args.ele.xpath)
                // label -> input
                if (DomUtil.matchElementType(element, TAGS.LABEL)) {
                    var forId = element.htmlFor;
                    if (forId != null && forId.length !== 0) {
                        var targetElement = element.ownerDocument.getElementById(forID);
                        if (targetElement != null) {
                            element = targetElement;
                        }
                    }
                }
                const status = element.checked
                if (typeof status === "boolean") {
                    return status
                } else {
                    throw new WebAutoError('无法读取此元素的勾选状态')
                }
            },
            WaitEleShowUp: (args) => { 
                const elements = DomUtil.GetEleByXpath(args.ele.xpath)
                if (!elements || elements.length === 0) {
                    return false;
                } else { 
                    return true;
                }
            },

            WaitEleDisapper: (args) => { 
                const elements = DomUtil.GetEleByXpath(args.ele.xpath)
                if (!elements || elements.length === 0) {
                    return true;
                } else { 
                    return false;
                }
            },

            scrollTo: (args) => {
                // elementId有可能为空
                // location (ScrollLocation)

                let element = null
                //如果参数有元素信息则滚动元素否则滚动整个window
                if (args.ele) {
                    element = DomUtil.GetEleByXpath(args.ele.xpath)
                } else {
                    element = window
                }
                //向上寻找带有滚动条的元素直到document
                if (args.searchUp) {
                    // 滚动元素只有纵向滚动
                    element = DomUtil.GetScrollableParent(element, "vertical");
                }

                if (args.location == 'Point') {
                    element.scrollTo({
                        top: args.top,
                        left: args.left,
                        behavior: args.behavior
                    })
                } else if (args.location == 'Top') {
                    element.scrollTo({
                        top: 0,
                        behavior: args.behavior
                    })
                } else if (args.location == 'Bottom') {
                    element.scrollTo({
                        top: element == window ? document.body.scrollHeight : element.scrollHeight,
                        behavior: args.behavior
                    })
                } else if (args.location == 'Screen') {
                    element.scrollTo({
                        top: document.documentElement.scrollTop + document.documentElement.clientHeight,
                        behavior: args.behavior
                    })
                } else { }
            },
        }
        this.invoke = function (actionName, args) {
            function failure(message) {
                return {
                    status: 'failure',
                    message: message
                }
            }
            try {
                let action = actions[actionName]
                if (!action) {
                    return failure(`找不到指定的Action, ${actionName}`)
                }
                let result = action.call(window, args)
                if (result instanceof SubFrame) {
                    return {
                        status: 'subframe',
                        route: result
                    }
                } else {
                    return {
                        status: 'success',
                        result: result
                    }
                }
            } catch (error) {
                console.error(error.stack);
                if (error instanceof WebAutoError) {
                    return failure(error.message)
                } else { //未知错误
                    return failure(error.stack || error.message || error)
                }
            }
        }
    }
})()
